16. Exercise: Add the Detail Screen
L8 30 Detail Screen SC
Update Note:
In the video above, inDetailFragment.ktfile, insideonCreateView()function, it shows the following two deprecated lines of code:
- At timestamp 05:51, the way to set the binding lifecycle to itself has changed from
binding.setLifecycleOwner(this)tobinding.lifecycleOwner = this.
- At timestamp 06:43, the ViewModelProviders class shown is now deprecated
binding.viewModel = ViewModelProviders.of( this, viewModelFactory).get(DetailViewModel::class.java).
Instead, please use the ViewModelProvider as shown in the instructions below.
With all those properties to choose from, we sure could use a little more information to help us decide.
In this exercise, you'll add a detail screen that displays more information for a Mars property. You'll also add all the usual infrastructure for adding a click listener and navigating to the new screen.
There are a lot of steps, but we'll break them up so you can build and run your code in between. If you want to start at this step, you can download the code for this exercise from: Step.07-Exercise-Adding-the-Detail-Screen. You will find plenty of //TODO comments to help you complete things, which may be particularly useful here given how many steps we have.
We'll start by updating the ViewModel code and adding some data binding to the fragment layout:
In
DetailViewModel, remove the@Suppress("UNUSED_PARAMETER")annotation from the class declaration.
Add an encapsulated
selectedPropertyLiveDatavariable, then set its value in aninitblock:
private val _selectedProperty = MutableLiveData<MarsProperty>()
val selectedProperty: LiveData<MarsProperty>
get() = _selectedProperty
init {
_selectedProperty.value = marsProperty
}
- In
fragment_detail.xml, add a<data>block and declare aviewModel<variable>of typeDetailViewModel:
<data>
<variable
name="viewModel"
type="com.example.android.marsrealestate.detail.DetailViewModel" />
</data>
- In the
main_photo_imageImageView, add anapp:imageUrlattribute that binds to theimgSrcUrlfor theselectedProperty:
app:imageUrl="@{viewModel.selectedProperty.imgSrcUrl}"
- Bind the
property_type_textTextViewtoviewModel.selectedProperty.type:
android:text="@{viewModel.selectedProperty.type}"
and the price_value_text TextView to viewModel.selectedProperty.price, converted to a string value:
android:text="@{String.valueOf(viewModel.selectedProperty.price)}"
- Build and run your code here, just to make sure you're on track. Nothing should change yet.
Add navigation code to the ViewModel:
- In
OverviewViewModel, add an encapsulatedLiveDatavariable for navigating to theselectedPropertydetail screen:
private val _navigateToSelectedProperty = MutableLiveData<MarsProperty>()
val navigateToSelectedProperty: LiveData<MarsProperty>
get() = _navigateToSelectedProperty
- Add a function to set
_navigateToSelectedPropertytomarsPropertyand initiate navigation to the detail screen on button click:
fun displayPropertyDetails(marsProperty: MarsProperty) {
_navigateToSelectedProperty.value = marsProperty
}
and you'll need to add displayPropertyDetailsComplete() to set _navigateToSelectedProperty to false once navigation is completed to prevent unwanted extra navigations:
fun displayPropertyDetailsComplete() {
_navigateToSelectedProperty.value = null
}
- Build your code and verify it still runs correctly. We've now got the structure in place in the ViewModel we'll need to trigger Navigation.
Next add a click listener:
In
PhotoGridAdapter, create an internalOnClickListenerclass with a lambda in its constructor that initializes a matchingonClickfunction:class OnClickListener(val clickListener: (marsProperty: MarsProperty) -> Unit) { fun onClick(marsProperty:MarsProperty) = clickListener(marsProperty) }Add an
OnClickListenerparameter to thePhotoGridAdapterclass declaration:class PhotoGridAdapter(val onClickListener: OnClickListener) : ListAdapter<MarsProperty, PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {In
onBindViewHolder(), set uponClickListener()to passmarsPropertyon button click:holder.itemView.setOnClickListener { onClickListener.onClick(marsProperty) }In
OverviewFragment, update thePhotosGridAdapterbinding to add a click listener that passes the selected property toviewModel.displayPropertyDetails():binding.photosGrid.adapter = PhotoGridAdapter(PhotoGridAdapter.OnClickListener { viewModel.displayPropertyDetails(it) })Build and run your project here. If you set a breakpoint in the debugger, you can see that the click handler in onCreateView is being called when you tap on an image,
Make MarsProperty parcelable so it can be passed as an argument in navigation :
- In
MarsProperty, make the class parcelable by extending it fromParcelableand adding the@Parcelizeannotation:
@Parcelize
data class MarsProperty(
val id: String,
@Json(name = "img_src") val imgSrcUrl: String,
val type: String,
val price: Double) : Parcelable {
}
In
nav_graph.xml, add an argumentselectedPropertyof typeMarsPropertytodetailFragment. You can do it in the navigation editor, but the xml should look like this:<argument android:name="selectedProperty" app:argType="com.example.android.marsrealestate.network.MarsProperty"/>
Build the project to generate the SafeArgs actions. Finish up the navigation to the Detail screen
In
OverviewFragment, add an observer onnavigateToSelectedPropertythat callsnavigate()to go to the detail screen when theMarsPropertyis notnull.and of course, we can't forget to call
displayPropertyDetailsComplete()when navigation is done:viewModel.navigateToSelectedProperty.observe(this, Observer { if ( null != it ) { this.findNavController().navigate(OverviewFragmentDirections.actionShowDetail(it)) viewModel.displayPropertyDetailsComplete() } })In
DetailFragment, inonCreateView()create amarsPropertyvariable from theDetailFragmentArgsarguments, then use it to create aDetailViewModelFactory:
val marsProperty = DetailFragmentArgs.fromBundle(arguments!!).selectedProperty
val viewModelFactory = DetailViewModelFactory(marsProperty, application)
Use the factory to create a
DetailViewModeland bind it to theviewModel:binding.viewModel = ViewModelProvider( this, viewModelFactory).get(DetailViewModel::class.java)Build and run the app and click on an image to open the Detail screen. Wow, great job!
And finally, the Details screen could use some improved formatting when displaying property information:
- Inside the
MarsPropertyclass, create anisRentalboolean, and set its value based on whether the property type is "rent":
val isRental
get() = type == "rent"
In
DetailViewModel, create a transformation map,displayPropertyPrice, to convertselectedProperty's price to a displayable string:val displayPropertyPrice = Transformations.map(selectedProperty) { app.applicationContext.getString( when (it.isRental) { true -> R.string.display_price_monthly_rental false -> R.string.display_price }, it.price) }and a second transformation map,
displayPropertyType, to display whetherselectedPropertyis for sale or rent:val displayPropertyType = Transformations.map(selectedProperty) { app.applicationContext.getString(R.string.display_type, app.applicationContext.getString( when(it.isRental) { true -> R.string.type_rent false -> R.string.type_sale })) }Replace the
TextViewandroid:textbindings infragment_detailwith the transformations defined in theviewModel
android:text="@{viewModel.displayPropertyType}"
and the same for price_value_text
android:text="@{viewModel.displayPropertyPrice}"
- Run the app and select a property to see its details. Now you can see how much it costs, and if it's for sale or rent, to help you better decide whether a permanent move to Mars is in your future…
If you get stuck, go back and watch the video again. Once you’re done, you can check your solution against the solution we’ve provided here: Step.07-Solution-Adding-the-Detail-Screen, or using this git diff.
Task Description:
Complete the tasks below to add a detail screen to display a Mars listing.
Task Feedback:
Great job! Give yourself a big pat on the back!